
/* GCSx
** TEXTURE.CPP
**
** Texture-management for OpenGL
*/

/*****************************************************************************
** Copyright (C) 2003-2006 Janson
**
** This program is free software; you can redistribute it and/or modify
** it under the terms of the GNU General Public License as published by
** the Free Software Foundation; either version 2 of the License, or
** (at your option) any later version.
** 
** This program is distributed in the hope that it will be useful,
** but WITHOUT ANY WARRANTY; without even the implied warranty of
** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
** GNU General Public License for more details.
**
** You should have received a copy of the GNU General Public License
** along with this program; if not, write to the Free Software
** Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111, USA.
*****************************************************************************/

#include "all.h"

// TextureMap overview:
// You specify how many graphics to store, and the size (all the same size)
// TextureMap will then allocate anything from a portion of an existing texture
// up to multiple textures.
// An alternate mode of operation simply allocates one texture per graphic to store.
// Partial updates to graphics are cached and all updates sent to video memory at once
// upon request.

vector<TextureMap::TextureList>* TextureMap::globalTextures = NULL;
TextureMap* TextureMap::headUpdate = NULL;
GLuint TextureMap::lastTexture = 0;

void TextureMap::setupOptimizations() { start_func
    lastTexture = 0;
}

int TextureMap::optionalPowerOfTwo(int val) { start_func
    assert(val >= 1);
    assert(val <= config->readNum(OGL_MAX_SIZE));

    int minSize = config->readNum(OGL_MIN_SIZE);
    
    if (val < minSize) return minSize;
    if (!config->readNum(OGL_POWER_OF_TWO)) return val;
    int bit = 1;
    while (bit < val) {
        bit <<= 1;
    }
    return bit;
}

int TextureMap::groupTextures(int count, int w, int h, vector<tGroup>& sizes) {
    sizes.clear();

    int startW = optionalPowerOfTwo(w);
    int startH = optionalPowerOfTwo(h);
    int max = config->readNum(OGL_MAX_SIZE);

    // If only one graphic, or requested, or size too large, we just go with 1 per!
    if ((count == 1) || (config->readNum(OGL_FORCE_SINGLE)) ||
        ((startW * 2 > max) && (startH * 2 > max))) {
        tGroup group;
        group.texW = startW;
        group.texH = startH;
        group.countW = 1;
        group.countH = 1;
        group.qty = count;
        sizes.push_back(group);
        return count;
    }
    else if (config->readNum(OGL_PREFER_GROUPING)) {
        // Find smallest sizing that holds *everything*, if possible
        int largestCount = (max / w) * (max / h);
        int total = 0;
        // Too many for one texture?
        if (count > largestCount) {
            tGroup group;
            group.texW = optionalPowerOfTwo((max / w) * w);
            group.texH = optionalPowerOfTwo((max / h) * h);
            group.countW = max / w;
            group.countH = max / h;
            group.qty = count / largestCount;
            sizes.push_back(group);
            count -= group.qty * largestCount;
            total += group.qty;
        }
        if (count) {
            int optimalSize = -1;
            int optimalW, optimalH;
            if (config->readNum(OGL_POWER_OF_TWO)) {
                for (int testW = startW; testW <= max; testW *= 2) {
                    int countW = testW / w;
                    for (int testH = startH; testH <= testW; testH *= 2) {
                        int total = countW * (testH / h);
                        if (count <= total) {
                            if ((testW * testH < optimalSize) || (optimalSize == -1)) {
                                optimalSize = testW * testH;
                                optimalW = testW;
                                optimalH = testH;
                                // Abort loop if perfect match found
                                if (total == count) {
                                    testW = max;
                                    break;
                                }
                            }
                        }
                    }
                }
            }
            else {
                int attempts = 0;
                for (int testW = w; testW <= max; testW += w) {
                    int countW = testW / w;
                    for (int testH = h; testH <= max; testH += h) {
                        ++attempts;
                        int total = countW * (testH / h);
                        if (count <= total) {
                            if ((testW * testH < optimalSize) || (optimalSize == -1)) {
                                optimalSize = testW * testH;
                                optimalW = testW;
                                optimalH = testH;
                                // Abort loop if perfect match found
                                if (total == count) {
                                    testW = max;
                                    break;
                                }
                            }
                        }
                    }
                }
            }
            tGroup group;
            group.texW = optimalW;
            group.texH = optimalH;
            group.countW = optimalW / w;
            group.countH = optimalH / h;
            group.qty = 1;
            sizes.push_back(group);
            ++total;
        }
        return total;
    }
    else {
        // Find largest sizing that generates the least waste
        // This isn't 100% optimal, but a decent compromise
        int optimalWaste = -1;
        int optimalW, optimalH, optimalFG, optimalLO;
        for (int testW = startW; testW <= max; testW *= 2) {
            int countW = testW / w;
            for (int testH = startH; testH <= testW; testH *= 2) {
                int countH = testH / h;
                // Calculate edge waste on one full grouping
                int waste = testW * testH - countW * w * countH * h;
                // Calculate edge waste for all full groupings
                int fullGroups = count / (countW * countH);
                waste *= fullGroups;
                // Calculate final grouping, if any
                int leftOver = count - (fullGroups * countW * countH);
                if (leftOver) {
                    int finalH = optionalPowerOfTwo((leftOver + countW - 1) / countW * h);
                    int fCountH = finalH / h;
                    int finalW = optionalPowerOfTwo((leftOver + fCountH - 1) / fCountH * w);
                    waste += finalW * finalH - leftOver * w * h;
                }
                if ((waste < optimalWaste) || (optimalWaste == -1) ||
                    ((waste == optimalWaste) && (testW * testH > optimalW * optimalH))) {
                    optimalWaste = waste;
                    optimalW = testW;
                    optimalH = testH;
                    optimalFG = fullGroups;
                    optimalLO = leftOver;
                }
            }
        }
        if (optimalFG) {
            tGroup group;
            group.texW = optimalW;
            group.texH = optimalH;
            group.countW = optimalW / w;
            group.countH = optimalH / h;
            group.qty = optimalFG;
            sizes.push_back(group);
        }
        if (optimalLO) {
            tGroup group;
            group.countW = optimalW / w;
            group.texH = optionalPowerOfTwo((optimalLO + group.countW - 1) / group.countW * h);
            group.countH = group.texH / h;
            group.texW = optionalPowerOfTwo((optimalLO + group.countH - 1) / group.countH * h);
            group.countW = group.texW / w;
            group.qty = 1;
            sizes.push_back(group);
        }
        return optimalFG + (optimalLO ? 1 : 0);
    }
}

TextureMap::TextureMap(int count, int width, int height,
                       void (*tileCoords)(int position, const SDL_Surface*& src, int& x, int& y)
                       ) : updates() { start_func
    int max = config->readNum(OGL_MAX_SIZE);
    nextUpdate = NULL;
    prevUpdate = NULL;
    graphics = NULL;
    
    dispWidth = width;
    dispHeight = height;
    graphicCount = count;
    
    GLuint* texNames = NULL;
    Uint8* data = NULL;
    
    if ((width > max) || (height > max)) {
    	// Graphic is too large for one texture
    	multiTexture = ((max + width - 1) / width) * ((max + height - 1) / height);
    	graphics = new TexturePos[multiTexture * (count + 1)];
    	
    	// @TODO:
    	assert(0);
    }
    else {
    	// One texture has room for multiple graphics (or one-per)
    	// Determine size(s)
    	multiTexture = 1;
    	graphics = new TexturePos[count + 1];
    	vector<tGroup> sizes;
    	int numTextures = groupTextures(count, width, height, sizes);
    	
    	// Allocate textures
    	texNames = new GLuint[numTextures];
    	
    	// Generate textures
    	// @TODO: Error checking?
        glGenTextures(numTextures, texNames);
        int tNum = 0;
        int gNum = 1;
        int done = 0;
        // Loop through texture groups
        for (vector<tGroup>::iterator pos = sizes.begin(); (!done) && (pos != sizes.end()); ++pos) {
            // Create texture data
            data = new Uint8[(*pos).texW * (*pos).texH * 4];
            // Loop through individual textures
            for (int tPos = 0; tPos < (*pos).qty; ++tPos, ++tNum) {
                // Technically not required, texture display should never
                // exceed bounds of known textures
                memset(data, 0, (*pos).texW * (*pos).texH * 4);
                
                // Assign to graphics
                int numW = (*pos).countW;
                int numH = (*pos).countH;
                GLfloat texW = (*pos).texW;
                GLfloat texH = (*pos).texH;
                int noSubTex = 0;
                if ((numW == 1) && (numH == 1)) noSubTex = 1;
                for (int y = 0; y < numH; ++y) {
                    for (int x = 0; x < numW; ++x) {
                        // Blit texture to data?
                        if (tileCoords) {
                            const SDL_Surface* src;
                            int gx, gy;
                            tileCoords(gNum, src, gx, gy);
                            matrixCopy((Uint8*)src->pixels + gx * 4 + gy * src->pitch,
                                       data + x * width * 4 + y * (*pos).texW * height * 4,
                                       width * 4, height,
                                       src->pitch, (*pos).texW * 4);
                        }
                        
                        // Remember subtexture stats
                        graphics[gNum].tex = texNames[tNum];
                        graphics[gNum].subtex = noSubTex ? -1 : (x + y * numW);
                        graphics[gNum].x1 = (GLfloat)(x * width) / texW;
                        graphics[gNum].x2 = (GLfloat)((x + 1) * width) / texW;
                        graphics[gNum].y1 = (GLfloat)(y * height) / texH;
                        graphics[gNum].y2 = (GLfloat)((y + 1) * height) / texH;
                        graphics[gNum].x = x * width;
                        graphics[gNum].y = y * height;

                        if (++gNum > count) {
                            // Abort all loops if done
                            y = numH;
                            tPos = (*pos).qty;
                            done = 1;
                            break;
                        }
                    }
                }
                
                // Create individual texture
                // @TODO: Error checking?
                glBindTexture(GL_TEXTURE_2D, texNames[tNum]);
                glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
                glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
                glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, (*pos).texW, (*pos).texH, 0, GL_RGBA, GL_UNSIGNED_BYTE, data);
                // @TODO: Reference in globalTextures
            }
            delete[] data;
        }
    }
    
    delete[] texNames;
}

TextureMap::~TextureMap() { start_func
	// @TODO: remove our textures from globalTextures
	// @TODO: deallocate our textures
	// when deallocating, clear lastTexture if matches
	delete[] graphics;
	removeFromUpdates();
}

void TextureMap::removeFromUpdates() { start_func
	if (nextUpdate) {
		// (safe even if we're pointing to ourselves)
		nextUpdate->prevUpdate = prevUpdate;
		prevUpdate->nextUpdate = nextUpdate;

		if (headUpdate == this) {
			if (nextUpdate == this) headUpdate = NULL;
			else headUpdate = nextUpdate;
		}
	}
}

void TextureMap::store(int graphic, const SDL_Surface* src, int sx, int sy, int dx, int dy, int dw, int dh) { start_func
    // @TODO: doesn't actually cache anything
    if (dw == -1) dw = dispWidth;
    if (dh == -1) dh = dispHeight;
    assert(dx >= 0);
    assert(dy >= 0);
    assert(dx + dw <= dispWidth);
    assert(dy + dh <= dispHeight);
    assert(dw >= 0);
    assert(dh >= 0);
    // Create texture data
    Uint8* data = new Uint8[dw * dh * 4];
    matrixCopy((Uint8*)src->pixels + sx * 4 + sy * src->pitch, data,
               dw * 4, dh,
               src->pitch, dw * 4);
    // @TODO: Error checking?
    glBindTexture(GL_TEXTURE_2D, graphics[graphic].tex);
    glTexSubImage2D(GL_TEXTURE_2D, 0, graphics[graphic].x + dx, graphics[graphic].y + dy,
                    dw, dh, GL_RGBA, GL_UNSIGNED_BYTE, data);
    delete[] data;
}

void TextureMap::updateTexture() { start_func
    // @TODO:
	removeFromUpdates();
}

void TextureMap::updateAllTextures() { start_func
    // @TODO:
}

void TextureMap::draw(int graphic, GLint x, GLint y) const { start_func
    assert(graphic > 0);
    assert(graphic <= graphicCount);

    // @TODO: Entire function- error checking?
    if (graphics[graphic].tex != lastTexture) {
        lastTexture = graphics[graphic].tex;
        glBindTexture(GL_TEXTURE_2D, lastTexture);
    }

    glBegin(GL_QUADS);

    glTexCoord2f(graphics[graphic].x1, graphics[graphic].y1); glVertex3i(x, y, 0);
    glTexCoord2f(graphics[graphic].x2, graphics[graphic].y1); glVertex3i(x + dispWidth, y, 0);
    glTexCoord2f(graphics[graphic].x2, graphics[graphic].y2); glVertex3i(x + dispWidth, y + dispHeight, 0);
    glTexCoord2f(graphics[graphic].x1, graphics[graphic].y2); glVertex3i(x, y + dispHeight, 0);

    glEnd();
}

void TextureMap::drawScale(int graphic, GLfloat x, GLfloat y, GLfloat scale) const { start_func
    assert(graphic > 0);
    assert(graphic <= graphicCount);

    // @TODO: Entire function- error checking?
    if (graphics[graphic].tex != lastTexture) {
        lastTexture = graphics[graphic].tex;
        glBindTexture(GL_TEXTURE_2D, lastTexture);
    }

    glBegin(GL_QUADS);

    GLfloat w = scale * dispWidth;
    GLfloat h = scale * dispHeight;
    
    glTexCoord2f(graphics[graphic].x1, graphics[graphic].y1); glVertex3f(x, y, 0);
    glTexCoord2f(graphics[graphic].x2, graphics[graphic].y1); glVertex3f(x + w, y, 0);
    glTexCoord2f(graphics[graphic].x2, graphics[graphic].y2); glVertex3f(x + w, y + h, 0);
    glTexCoord2f(graphics[graphic].x1, graphics[graphic].y2); glVertex3f(x, y + h, 0);

    glEnd();
}

void TextureMap::draw(int graphic, GLint x, GLint y, int orient) const { start_func
    assert(graphic > 0);
    assert(graphic <= graphicCount);

    // @TODO: Entire function- error checking?
    if (graphics[graphic].tex != lastTexture) {
        lastTexture = graphics[graphic].tex;
        glBindTexture(GL_TEXTURE_2D, lastTexture);
    }

    GLfloat x1 = graphics[graphic].x1;
    GLfloat x2 = graphics[graphic].x2;
    GLfloat y1 = graphics[graphic].y1;
    GLfloat y3 = graphics[graphic].y2;

    if (orient & TEXTURE_FLIP) swap(y1, y3);
    if (orient & TEXTURE_MIRROR) swap(x1, x2);

    GLfloat x3 = x2, x4, y2 = y1, y4 = y3;
    if (orient & TEXTURE_ROTATE) {
        x4 = x2;
        x2 = x1;
        swap(y1, y3);
    }
    else {
        x4 = x1;
    }

    glBegin(GL_QUADS);

    glTexCoord2f(x1, y1); glVertex3i(x, y, 0);
    glTexCoord2f(x2, y2); glVertex3i(x + dispWidth, y, 0);
    glTexCoord2f(x3, y3); glVertex3i(x + dispWidth, y + dispHeight, 0);
    glTexCoord2f(x4, y4); glVertex3i(x, y + dispHeight, 0);

    glEnd();
}

void TextureMap::draw(int graphic, GLint x, GLint y, Rect clip) const { start_func
    assert(graphic > 0);
    assert(graphic <= graphicCount);

    // @TODO:
    assert(0);
}
